import iconv from 'iconv-lite';
import killTree from 'tree-kill';

import { ChildProcess } from 'child_process';
import { isWin, executeCommand, ChunkSplitTransform, logger, pidIsRunning } from '$common';

type Environments = { [x: string]: string }

export type ExecutedResult = {
  pid?: number;
  exitCode: number;
  errorMsg?: string;
}
export class CommandExecuteBuilder {

  public timeout: number;
  public workDir: string;
  public envs: Environments;

  public logger: any = logger;

  setTimeout(timeout: number) {
    this.timeout = timeout;
    return this;
  }

  setWorkDir(cwd: string) {
    this.workDir = cwd;
    return this;
  }

  setLogger(logger: any) {
    this.logger = logger;
    return this;
  }

  setEnvironments(envs: Environments) {
    this.envs = envs;
    return this;
  }

  build() {
    return new CommandExecutor({
      envs: this.envs,
      logger: this.logger,
      timeout: this.timeout,
      workdir: this.workDir
    });
  }
}

export interface ICommandExecutor {
  execute(commandLine: string): Promise<ExecutedResult>
  terminate(killProcessTree: boolean): void;
}

type ExecutorOptions = {
  envs?: Environments
  timeout?: number,
  workdir?: string,
  logger?: any
}

export class CommandExecutor implements ICommandExecutor {

  private _pid: number;
  private _envs: Environments;
  private _workDir: string;
  private _logger: any;

  constructor(options: ExecutorOptions) {
    this._workDir = options.workdir;
    this._logger = options.logger || logger;
    this._envs = options.envs;
  }

  private async killProcessTeee(pid: number) {
    return new Promise((resolve, reject) => {
      killTree(pid, (err) => {
        if (err) {
          reject(err);
          return;
        }
        resolve('');
      });
    });
  }

  async terminate(killProcessTree = true) {
    if (!this._pid) {
      this._logger.debug('⛔ Cannot kill a process where pid does not exist.');
      return;
    }

    if (!pidIsRunning(this._pid)) {
      this._logger.debug(`⛔ Cannot kill a process where pid(${this._pid}) is not running.`);
      return;
    }

    if (killProcessTree) {
      this._logger.debug(`⛔ killing process tree by pid(pid=${this._pid})`);
      await this.killProcessTeee(this._pid);
    } else {
      this._logger.debug(`⛔ killing process by pid(${this._pid})`);
      process.kill(this._pid);
    }
  }

  async execute(commandLine: string): Promise<ExecutedResult> {
    this._logger.info(`🚀 Execute command: ${commandLine}, working directory: ${this._workDir}`);

    const childProcess: ChildProcess = executeCommand(commandLine, this._workDir, this._envs);
    const pid = this._pid = childProcess.pid;

    return new Promise((resolve, reject) => {
      const errorMsgs = [];

      const stdoutChunker = new ChunkSplitTransform({ separator: '\n' });
      const stderrChunker = new ChunkSplitTransform({ separator: '\n' });

      stdoutChunker.on('data', (chunk: string | Buffer) => {
        this._logger.debug(chunk.toString());
      });
      stderrChunker.on('data', (chunk: string | Buffer) => {
        errorMsgs.push(chunk);
      });
  
      childProcess.on('error', (err) => {
        this._logger.error(`failed start command "${commandLine}": ${err}`);
        reject(err);
      });
  
      childProcess.on('exit', (code, signal) => {
        this._logger.info(`🔚pid(${pid}) exited , command '${commandLine}' exited with code: ${code}, signal: ${signal}`);
        let errorMsgStr = '';
        if (errorMsgs.length !== 0) {
          errorMsgStr = errorMsgs.join('\n');
          this._logger.error(errorMsgStr);
        }
        resolve({ pid, exitCode: code, errorMsg: errorMsgStr });
      });

      if (isWin) {
        childProcess.stdout.pipe(iconv.decodeStream('cp936')).pipe(stdoutChunker);
        childProcess.stderr.pipe(iconv.decodeStream('cp936')).pipe(stderrChunker);
      } else {
        childProcess.stdout.pipe(stdoutChunker);
        childProcess.stderr.pipe(stderrChunker);
      }
    });
  }
}